From 7671b2b71e28fa013917f65a1d49828ebb6a9725 Mon Sep 17 00:00:00 2001 From: Antoine Musso Date: Wed, 25 Jan 2017 23:29:40 +0100 Subject: [PATCH] Drop in replacement of eval.php based on psysh MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit eval.php is meant to eval() commands in MediaWiki global scope. We had at least a couple attempts to move it to a regular Maintenance script. As noted on the revert commit b475e930 r54839, using a Maintenance script drop us in the callee function scope instead of the global scope which is a hard requirement. http://psysh.org/ is a Read-Eval-Print-Loop with color highlight, modern code and more. Add maintenance/shell.php script as MediaWikiShell class. Passing command from stdin is supported. Execution is forked for graceful handling of PHP fatal errors. Colors!! Support the undocumented '-d' arguments from eval.php: 0 set $wgDebugLogFile to stdout. Eval.php used /dev/stdout, make it php://stdout in the new script. 1 additionally set DBO_DEBUG on all the database load balancer servers PHP globals have to be whitelisted which is not supported out of the box by Psy. Upon request, the author of PsySh, Justin Hileman, kindly provided a pass to inject globals (with the exceptions of super globals). He agreed for code reuse: https://github.com/bobthecow/psysh/issues/353 The code was added to maintenance/CodeCleanerGlobalsPass.inc Note that this is not a perfect simulation of global scope (but pretty close): variables declared in the shell which did not exist before won't be global unless the 'global' keword is used. Example usage: $ php maintenance/shell.php >>> $wgFullyInitialised => true >>> $hashar = User::newFromName( 'hashar' ) => User {#274 +mId: null, +mName: "Hashar", ... >>> ls --long --all $h Bug: T117661 Signed-off-by: Justin Hileman Signed-off-by: Gergő Tisza Change-Id: I3d6d42e138d3cc4a0aaafdd7f5f97cb17d8b8cb3 --- autoload.php | 2 + composer.json | 3 +- maintenance/CodeCleanerGlobalsPass.inc | 52 +++++++++++++ maintenance/shell.php | 102 +++++++++++++++++++++++++ tests/phan/config.php | 9 ++- 5 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 maintenance/CodeCleanerGlobalsPass.inc create mode 100644 maintenance/shell.php diff --git a/autoload.php b/autoload.php index d4bd447d4f..7f47d49817 100644 --- a/autoload.php +++ b/autoload.php @@ -257,6 +257,7 @@ $wgAutoloadLocalClasses = [ 'ClearInterwikiCache' => __DIR__ . '/maintenance/clearInterwikiCache.php', 'CliInstaller' => __DIR__ . '/includes/installer/CliInstaller.php', 'CloneDatabase' => __DIR__ . '/includes/db/CloneDatabase.php', + 'CodeCleanerGlobalsPass' => __DIR__ . '/maintenance/CodeCleanerGlobalsPass.inc', 'CodeContentHandler' => __DIR__ . '/includes/content/CodeContentHandler.php', 'Collation' => __DIR__ . '/includes/collation/Collation.php', 'CollationCkb' => __DIR__ . '/includes/collation/CollationCkb.php', @@ -812,6 +813,7 @@ $wgAutoloadLocalClasses = [ 'MediaTransformOutput' => __DIR__ . '/includes/media/MediaTransformOutput.php', 'MediaWiki' => __DIR__ . '/includes/MediaWiki.php', 'MediaWikiI18N' => __DIR__ . '/includes/skins/MediaWikiI18N.php', + 'MediaWikiShell' => __DIR__ . '/maintenance/shell.php', 'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php', 'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php', 'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php', diff --git a/composer.json b/composer.json index d41492ef6b..9a9cb7c616 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,8 @@ "phpunit/phpunit": "4.8.31", "wikimedia/avro": "1.7.7", "hamcrest/hamcrest-php": "^2.0", - "wmde/hamcrest-html-matchers": "^0.1.0" + "wmde/hamcrest-html-matchers": "^0.1.0", + "psy/psysh": "0.8.1" }, "suggest": { "ext-apc": "Local data and opcode cache", diff --git a/maintenance/CodeCleanerGlobalsPass.inc b/maintenance/CodeCleanerGlobalsPass.inc new file mode 100644 index 0000000000..5e8e754796 --- /dev/null +++ b/maintenance/CodeCleanerGlobalsPass.inc @@ -0,0 +1,52 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Maintenance + * + * @author Justin Hileman + */ + +/** + * Prefix the real command with a bunch of 'global $VAR;' commands, one for each global. + * This will make the shell behave as if it was running in the global scope (almost; + * variables created in the shell won't become global if no global variable by that name + * existed before). + */ +class CodeCleanerGlobalsPass extends \Psy\CodeCleaner\CodeCleanerPass { + private static $superglobals = [ + 'GLOBALS', '_SERVER', '_ENV', '_FILES', '_COOKIE', '_POST', '_GET', '_SESSION' + ]; + + public function beforeTraverse( array $nodes ) { + $names = []; + foreach ( array_diff( array_keys( $GLOBALS ), self::$superglobals ) as $name ) { + array_push( $names, new \PhpParser\Node\Expr\Variable( $name ) ); + } + + array_unshift( $nodes, new \PhpParser\Node\Stmt\Global_( $names ) ); + + return $nodes; + } +} + diff --git a/maintenance/shell.php b/maintenance/shell.php new file mode 100644 index 0000000000..47ef8045b1 --- /dev/null +++ b/maintenance/shell.php @@ -0,0 +1,102 @@ + + * Copyright © 2017 Gergő Tisza + * Copyright © 2017 Justin Hileman + * Copyright © 2017 Wikimedia Foundation Inc. + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Maintenance + * + * @author Antoine Musso + * @author Justin Hileman + * @author Gergő Tisza + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Interactive shell with completion and global scope. + * + */ +class MediaWikiShell extends Maintenance { + + public function __construct() { + parent::__construct(); + $this->addOption( 'd', + 'For back compatibility with eval.php. ' . + '0 send debug to stdout. ' . + 'With 1 additionally initialize database with debugging ', + false, true + ); + } + + public function execute() { + if ( !class_exists( \Psy\Shell::class ) ) { + $this->error( 'PsySH not found. Please run composer with the --dev option.', 1 ); + } + + $traverser = new \PhpParser\NodeTraverser(); + $codeCleaner = new \Psy\CodeCleaner( null, null, $traverser ); + + // add this after initializing the code cleaner so all the default passes get added first + $traverser->addVisitor( new CodeCleanerGlobalsPass() ); + + $config = new \Psy\Configuration( [ 'codeCleaner' => $codeCleaner ] ); + $config->setUpdateCheck( \Psy\VersionUpdater\Checker::NEVER ); + $shell = new \Psy\Shell( $config ); + if ( $this->hasOption( 'd' ) ) { + $this->setupLegacy(); + } + + $shell->run(); + } + + /** + * For back compatibility with eval.php + */ + protected function setupLegacy() { + global $wgDebugLogFile; + + $d = intval( $this->getOption( 'd' ) ); + if ( $d > 0 ) { + $wgDebugLogFile = 'php://stdout'; + } + if ( $d > 1 ) { + # Set DBO_DEBUG (equivalent of $wgDebugDumpSql) + # XXX copy pasted from eval.php :( + $lb = wfGetLB(); + $serverCount = $lb->getServerCount(); + for ( $i = 0; $i < $serverCount; $i++ ) { + $server = $lb->getServerInfo( $i ); + $server['flags'] |= DBO_DEBUG; + $lb->setServerInfo( $i, $server ); + } + } + } + +} + +$maintClass = 'MediaWikiShell'; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/tests/phan/config.php b/tests/phan/config.php index c561f8174a..90acc39837 100644 --- a/tests/phan/config.php +++ b/tests/phan/config.php @@ -42,14 +42,15 @@ return [ class_exists( Memcached::class ) ? [] : [ 'tests/phan/stubs/memcached.php' ], [ 'maintenance/7zip.inc', - 'maintenance/backupPrefetch.inc', - 'maintenance/commandLine.inc', - 'maintenance/sqlite.inc', - 'maintenance/userOptions.inc', 'maintenance/backup.inc', + 'maintenance/backupPrefetch.inc', 'maintenance/cleanupTable.inc', + 'maintenance/CodeCleanerGlobalsPass.inc', + 'maintenance/commandLine.inc', 'maintenance/importImages.inc', + 'maintenance/sqlite.inc', 'maintenance/userDupes.inc', + 'maintenance/userOptions.inc', 'maintenance/language/checkLanguage.inc', 'maintenance/language/languages.inc', ] -- 2.20.1